<?php
/**
 * @version		$Id: table.php 118 2009-03-05 02:14:07Z sanglt $
 * @package		Joomla.Framework
 * @subpackage	Table
 * @copyright	Copyright (C) 2005 - 2008 Open Source Matters. All rights reserved.
 * @license		GNU/GPL, see LICENSE.php
 * Joomla! is free software. This version may have been modified pursuant
 * to the GNU General Public License, and as distributed it includes or
 * is derivative of works licensed under the GNU General Public License or
 * other free or open source software licenses.
 * See COPYRIGHT.php for copyright notices and details.
 */

// Check to ensure this file is within the rest of the framework
defined ( 'JPATH_BASE' ) or die ();

/**
 * Abstract Table class
 *
 * Parent classes to all tables.
 *
 * @abstract
 * @author		Andrew Eddie <eddieajau@users.sourceforge.net>
 * @package 	Joomla.Framework
 * @subpackage	Table
 * @since		1.0
 * @tutorial	Joomla.Framework/jtable.cls
 */
class JTable extends JObject {
	/**
	 * Name of the table in the db schema relating to child class
	 *
	 * @var 	string
	 * @access	protected
	 */
	var $_tbl = '';
	
	/**
	 * Name of the primary key field in the table
	 *
	 * @var		string
	 * @access	protected
	 */
	var $_tbl_key = '';
	
	/**
	 * Database connector
	 *
	 * @var		JDatabase
	 * @access	protected
	 */
	var $_db = null;
	
	/**
	 * Object constructor to set table and key field
	 *
	 * Can be overloaded/supplemented by the child class
	 *
	 * @access protected
	 * @param string $table name of the table in the db schema relating to child class
	 * @param string $key name of the primary key field in the table
	 * @param object $db JDatabase object
	 */
	function __construct($table, $key, &$db) {
		$this->_tbl = $table;
		$this->_tbl_key = $key;
		$this->_db = & $db;
	}
	
	/**
	 * Returns a reference to the a Table object, always creating it
	 *
	 * @param type 		$type 	 The table type to instantiate
	 * @param string 	$prefix	 A prefix for the table class name. Optional.
	 * @param array		$options Configuration array for model. Optional.
	 * @return database A database object
	 * @since 1.5
	 */
	function &getInstance($type, $prefix = 'JTable', $config = array()) {
		$false = false;
		
		$type = preg_replace ( '/[^A-Z0-9_\.-]/i', '', $type );
		$tableClass = $prefix . ucfirst ( $type );
		
		if (! class_exists ( $tableClass )) {
			jimport ( 'joomla.filesystem.path' );
			if ($path = JPath::find ( JTable::addIncludePath (), strtolower ( $type ) . '.php' )) {
				require_once $path;
				
				if (! class_exists ( $tableClass )) {
					JError::raiseWarning ( 0, 'Table class ' . $tableClass . ' not found in file.' );
					return $false;
				}
			} else {
				JError::raiseWarning ( 0, 'Table ' . $type . ' not supported. File not found.' );
				return $false;
			}
		}
		
		//Make sure we are returning a DBO object
		if (array_key_exists ( 'dbo', $config )) {
			$db = & $config ['dbo'];
		} else {
			$db = & JFactory::getDBO ();
		}
		
		$instance = new $tableClass ( $db );
		//$instance->setDBO($db);
		

		return $instance;
	}
	
	/**
	 * Get the internal database object
	 *
	 * @return object A JDatabase based object
	 */
	function &getDBO() {
		return $this->_db;
	}
	
	/**
	 * Set the internal database object
	 *
	 * @param	object	$db	A JDatabase based object
	 * @return	void
	 */
	function setDBO(&$db) {
		$this->_db = & $db;
	}
	
	/**
	 * Gets the internal table name for the object
	 *
	 * @return string
	 * @since 1.5
	 */
	function getTableName() {
		return $this->_tbl;
	}
	
	/**
	 * Gets the internal primary key name
	 *
	 * @return string
	 * @since 1.5
	 */
	function getKeyName() {
		return $this->_tbl_key;
	}
	
	/**
	 * Resets the default properties
	 * @return	void
	 */
	function reset() {
		$k = $this->_tbl_key;
		foreach ( $this->getProperties () as $name => $value ) {
			if ($name != $k) {
				$this->$name = $value;
			}
		}
	}
	
	/**
	 * Binds a named array/hash to this object
	 *
	 * Can be overloaded/supplemented by the child class
	 *
	 * @access	public
	 * @param	$from	mixed	An associative array or object
	 * @param	$ignore	mixed	An array or space separated list of fields not to bind
	 * @return	boolean
	 */
	function bind($from, $ignore = array()) {
		$fromArray = is_array ( $from );
		$fromObject = is_object ( $from );
		
		if (! $fromArray && ! $fromObject) {
			$this->setError ( get_class ( $this ) . '::bind failed. Invalid from argument' );
			return false;
		}
		if (! is_array ( $ignore )) {
			$ignore = explode ( ' ', $ignore );
		}
		foreach ( $this->getProperties () as $k => $v ) {
			// internal attributes of an object are ignored
			if (! in_array ( $k, $ignore )) {
				if ($fromArray && isset ( $from [$k] )) {
					$this->$k = $from [$k];
				} else if ($fromObject && isset ( $from->$k )) {
					$this->$k = $from->$k;
				}
			}
		}
		return true;
	}
	
	/**
	 * Loads a row from the database and binds the fields to the object properties
	 *
	 * @access	public
	 * @param	mixed	Optional primary key.  If not specifed, the value of current key is used
	 * @return	boolean	True if successful
	 */
	function load($oid = null) {
		$k = $this->_tbl_key;
		
		if ($oid !== null) {
			$this->$k = $oid;
		}
		
		$oid = $this->$k;
		
		if ($oid === null) {
			return false;
		}
		$this->reset ();
		
		$db = & $this->getDBO ();
		
		$query = 'SELECT *' . ' FROM ' . $this->_tbl . ' WHERE ' . $this->_tbl_key . ' = ' . $db->Quote ( $oid );
		$db->setQuery ( $query );
		
		if ($result = $db->loadAssoc ()) {
			return $this->bind ( $result );
		} else {
			$this->setError ( $db->getErrorMsg () );
			return false;
		}
	}
	
	/**
	 * Generic check method
	 *
	 * Can be overloaded/supplemented by the child class
	 *
	 * @access public
	 * @return boolean True if the object is ok
	 */
	function check() {
		return true;
	}
	
	/**
	 * Inserts a new row if id is zero or updates an existing row in the database table
	 *
	 * Can be overloaded/supplemented by the child class
	 *
	 * @access public
	 * @param boolean If false, null object variables are not updated
	 * @return null|string null if successful otherwise returns and error message
	 */
	function store($updateNulls = false) {
		$k = $this->_tbl_key;
		
		if ($this->$k) {
			$ret = $this->_db->updateObject ( $this->_tbl, $this, $this->_tbl_key, $updateNulls );
		} else {
			$ret = $this->_db->insertObject ( $this->_tbl, $this, $this->_tbl_key );
		}
		if (! $ret) {
			$this->setError ( get_class ( $this ) . '::store failed - ' . $this->_db->getErrorMsg () );
			return false;
		} else {
			return true;
		}
	}
	
	/**
	 * Description
	 *
	 * @access public
	 * @param $dirn
	 * @param $where
	 */
	function move($dirn, $where = '') {
		if (! in_array ( 'ordering', array_keys ( $this->getProperties () ) )) {
			$this->setError ( get_class ( $this ) . ' does not support ordering' );
			return false;
		}
		
		$k = $this->_tbl_key;
		
		$sql = "SELECT $this->_tbl_key, ordering FROM $this->_tbl";
		
		if ($dirn < 0) {
			$sql .= ' WHERE ordering < ' . ( int ) $this->ordering;
			$sql .= ($where ? ' AND ' . $where : '');
			$sql .= ' ORDER BY ordering DESC';
		} else if ($dirn > 0) {
			$sql .= ' WHERE ordering > ' . ( int ) $this->ordering;
			$sql .= ($where ? ' AND ' . $where : '');
			$sql .= ' ORDER BY ordering';
		} else {
			$sql .= ' WHERE ordering = ' . ( int ) $this->ordering;
			$sql .= ($where ? ' AND ' . $where : '');
			$sql .= ' ORDER BY ordering';
		}
		
		$this->_db->setQuery ( $sql, 0, 1 );
		
		$row = null;
		$row = $this->_db->loadObject ();
		if (isset ( $row )) {
			$query = 'UPDATE ' . $this->_tbl . ' SET ordering = ' . ( int ) $row->ordering . ' WHERE ' . $this->_tbl_key . ' = ' . $this->_db->Quote ( $this->$k );
			$this->_db->setQuery ( $query );
			
			if (! $this->_db->query ()) {
				$err = $this->_db->getErrorMsg ();
				JError::raiseError ( 500, $err );
			}
			
			$query = 'UPDATE ' . $this->_tbl . ' SET ordering = ' . ( int ) $this->ordering . ' WHERE ' . $this->_tbl_key . ' = ' . $this->_db->Quote ( $row->$k );
			$this->_db->setQuery ( $query );
			
			if (! $this->_db->query ()) {
				$err = $this->_db->getErrorMsg ();
				JError::raiseError ( 500, $err );
			}
			
			$this->ordering = $row->ordering;
		} else {
			$query = 'UPDATE ' . $this->_tbl . ' SET ordering = ' . ( int ) $this->ordering . ' WHERE ' . $this->_tbl_key . ' = ' . $this->_db->Quote ( $this->$k );
			$this->_db->setQuery ( $query );
			
			if (! $this->_db->query ()) {
				$err = $this->_db->getErrorMsg ();
				JError::raiseError ( 500, $err );
			}
		}
	}
	
	/**
	 * Returns the ordering value to place a new item last in its group
	 *
	 * @access public
	 * @param string query WHERE clause for selecting MAX(ordering).
	 */
	function getNextOrder($where = '') {
		if (! in_array ( 'ordering', array_keys ( $this->getProperties () ) )) {
			$this->setError ( get_class ( $this ) . ' does not support ordering' );
			return false;
		}
		
		$query = 'SELECT MAX(ordering)' . ' FROM ' . $this->_tbl . ($where ? ' WHERE ' . $where : '');
		
		$this->_db->setQuery ( $query );
		$maxord = $this->_db->loadResult ();
		
		if ($this->_db->getErrorNum ()) {
			$this->setError ( $this->_db->getErrorMsg () );
			return false;
		}
		return $maxord + 1;
	}
	
	/**
	 * Compacts the ordering sequence of the selected records
	 *
	 * @access public
	 * @param string Additional where query to limit ordering to a particular subset of records
	 */
	function reorder($where = '') {
		$k = $this->_tbl_key;
		
		if (! in_array ( 'ordering', array_keys ( $this->getProperties () ) )) {
			$this->setError ( get_class ( $this ) . ' does not support ordering' );
			return false;
		}
		
		if ($this->_tbl == '#__content_frontpage') {
			$order2 = ", content_id DESC";
		} else {
			$order2 = "";
		}
		
		$query = 'SELECT ' . $this->_tbl_key . ', ordering' . ' FROM ' . $this->_tbl . ' WHERE ordering >= 0' . ($where ? ' AND ' . $where : '') . ' ORDER BY ordering' . $order2;
		$this->_db->setQuery ( $query );
		if (! ($orders = $this->_db->loadObjectList ())) {
			$this->setError ( $this->_db->getErrorMsg () );
			return false;
		}
		// compact the ordering numbers
		for($i = 0, $n = count ( $orders ); $i < $n; $i ++) {
			if ($orders [$i]->ordering >= 0) {
				if ($orders [$i]->ordering != $i + 1) {
					$orders [$i]->ordering = $i + 1;
					$query = 'UPDATE ' . $this->_tbl . ' SET ordering = ' . ( int ) $orders [$i]->ordering . ' WHERE ' . $k . ' = ' . $this->_db->Quote ( $orders [$i]->$k );
					$this->_db->setQuery ( $query );
					$this->_db->query ();
				}
			}
		}
		
		return true;
	}
	
	/**
	 * Generic check for whether dependancies exist for this object in the db schema
	 *
	 * can be overloaded/supplemented by the child class
	 *
	 * @access public
	 * @param string $msg Error message returned
	 * @param int Optional key index
	 * @param array Optional array to compiles standard joins: format [label=>'Label',name=>'table name',idfield=>'field',joinfield=>'field']
	 * @return true|false
	 */
	function canDelete($oid = null, $joins = null) {
		$k = $this->_tbl_key;
		if ($oid) {
			$this->$k = intval ( $oid );
		}
		
		if (is_array ( $joins )) {
			$select = "$k";
			$join = "";
			foreach ( $joins as $table ) {
				$select .= ', COUNT(DISTINCT ' . $table ['idfield'] . ') AS ' . $table ['idfield'];
				$join .= ' LEFT JOIN ' . $table ['name'] . ' ON ' . $table ['joinfield'] . ' = ' . $k;
			}
			
			$query = 'SELECT ' . $select . ' FROM ' . $this->_tbl . $join . ' WHERE ' . $k . ' = ' . $this->_db->Quote ( $this->$k ) . ' GROUP BY ' . $k;
			$this->_db->setQuery ( $query );
			
			if (! $obj = $this->_db->loadObject ()) {
				$this->setError ( $this->_db->getErrorMsg () );
				return false;
			}
			$msg = array ();
			$i = 0;
			foreach ( $joins as $table ) {
				$k = $table ['idfield'] . $i;
				if ($obj->$k) {
					$msg [] = JText::_ ( $table ['label'] );
				}
				$i ++;
			}
			
			if (count ( $msg )) {
				$this->setError ( "noDeleteRecord" . ": " . implode ( ', ', $msg ) );
				return false;
			} else {
				return true;
			}
		}
		
		return true;
	}
	
	/**
	 * Default delete method
	 *
	 * can be overloaded/supplemented by the child class
	 *
	 * @access public
	 * @return true if successful otherwise returns and error message
	 */
	function delete($oid = null) {
		//if (!$this->canDelete( $msg ))
		//{
		//	return $msg;
		//}
		

		$k = $this->_tbl_key;
		if ($oid) {
			$this->$k = intval ( $oid );
		}
		
		$query = 'DELETE FROM ' . $this->_db->nameQuote ( $this->_tbl ) . ' WHERE ' . $this->_tbl_key . ' = ' . $this->_db->Quote ( $this->$k );
		$this->_db->setQuery ( $query );
		
		if ($this->_db->query ()) {
			return true;
		} else {
			$this->setError ( $this->_db->getErrorMsg () );
			return false;
		}
	}
	
	/**
	 * Checks out a row
	 *
	 * @access public
	 * @param	integer	The id of the user
	 * @param 	mixed	The primary key value for the row
	 * @return	boolean	True if successful, or if checkout is not supported
	 */
	function checkout($who, $oid = null) {
		if (! in_array ( 'checked_out', array_keys ( $this->getProperties () ) )) {
			return true;
		}
		
		$k = $this->_tbl_key;
		if ($oid !== null) {
			$this->$k = $oid;
		}
		
		$date = & JFactory::getDate ();
		$time = $date->toMysql ();
		
		$query = 'UPDATE ' . $this->_db->nameQuote ( $this->_tbl ) . ' SET checked_out = ' . ( int ) $who . ', checked_out_time = ' . $this->_db->Quote ( $time ) . ' WHERE ' . $this->_tbl_key . ' = ' . $this->_db->Quote ( $this->$k );
		$this->_db->setQuery ( $query );
		
		$this->checked_out = $who;
		$this->checked_out_time = $time;
		
		return $this->_db->query ();
	}
	
	/**
	 * Checks in a row
	 *
	 * @access	public
	 * @param	mixed	The primary key value for the row
	 * @return	boolean	True if successful, or if checkout is not supported
	 */
	function checkin($oid = null) {
		if (! (in_array ( 'checked_out', array_keys ( $this->getProperties () ) ) || in_array ( 'checked_out_time', array_keys ( $this->getProperties () ) ))) {
			return true;
		}
		
		$k = $this->_tbl_key;
		
		if ($oid !== null) {
			$this->$k = $oid;
		}
		
		if ($this->$k == NULL) {
			return false;
		}
		
		$query = 'UPDATE ' . $this->_db->nameQuote ( $this->_tbl ) . ' SET checked_out = 0, checked_out_time = ' . $this->_db->Quote ( $this->_db->getNullDate () ) . ' WHERE ' . $this->_tbl_key . ' = ' . $this->_db->Quote ( $this->$k );
		$this->_db->setQuery ( $query );
		
		$this->checked_out = 0;
		$this->checked_out_time = '';
		
		return $this->_db->query ();
	}
	
	/**
	 * Description
	 *
	 * @access public
	 * @param $oid
	 * @param $log
	 */
	function hit($oid = null, $log = false) {
		if (! in_array ( 'hits', array_keys ( $this->getProperties () ) )) {
			return;
		}
		
		$k = $this->_tbl_key;
		
		if ($oid !== null) {
			$this->$k = intval ( $oid );
		}
		
		$query = 'UPDATE ' . $this->_tbl . ' SET hits = ( hits + 1 )' . ' WHERE ' . $this->_tbl_key . '=' . $this->_db->Quote ( $this->$k );
		$this->_db->setQuery ( $query );
		$this->_db->query ();
		$this->hits ++;
	}
	
	/**
	 * Check if an item is checked out
	 *
	 * This function can be used as a static function too, when you do so you need to also provide the
	 * a value for the $against parameter.
	 *
	 * @static
	 * @access public
	 * @param integer  $with  	The userid to preform the match with, if an item is checked out
	 * 				  			by this user the function will return false
	 * @param integer  $against 	The userid to perform the match against when the function is used as
	 * 							a static function.
	 * @return boolean
	 */
	function isCheckedOut($with = 0, $against = null) {
		if (isset ( $this ) && is_a ( $this, 'JTable' ) && is_null ( $against )) {
			$against = $this->get ( 'checked_out' );
		}
		
		//item is not checked out, or being checked out by the same user
		if (! $against || $against == $with) {
			return false;
		}
		
		$session = & JTable::getInstance ( 'session' );
		return $session->exists ( $against );
	}
	
	/**
	 * Generic save function
	 *
	 * @access	public
	 * @param	array	Source array for binding to class vars
	 * @param	string	Filter for the order updating
	 * @param	mixed	An array or space separated list of fields not to bind
	 * @returns TRUE if completely successful, FALSE if partially or not succesful.
	 */
	function save($source, $order_filter = '', $ignore = '') {
		if (! $this->bind ( $source, $ignore )) {
			return false;
		}
		if (! $this->check ()) {
			return false;
		}
		if (! $this->store ()) {
			return false;
		}
		if (! $this->checkin ()) {
			return false;
		}
		if ($order_filter) {
			$filter_value = $this->$order_filter;
			$this->reorder ( $order_filter ? $this->_db->nameQuote ( $order_filter ) . ' = ' . $this->_db->Quote ( $filter_value ) : '' );
		}
		$this->setError ( '' );
		return true;
	}
	
	/**
	 * Generic Publish/Unpublish function
	 *
	 * @access public
	 * @param array An array of id numbers
	 * @param integer 0 if unpublishing, 1 if publishing
	 * @param integer The id of the user performnig the operation
	 * @since 1.0.4
	 */
	function publish($cid = null, $publish = 1, $user_id = 0) {
		JArrayHelper::toInteger ( $cid );
		$user_id = ( int ) $user_id;
		$publish = ( int ) $publish;
		$k = $this->_tbl_key;
		
		if (count ( $cid ) < 1) {
			if ($this->$k) {
				$cid = array ($this->$k );
			} else {
				$this->setError ( "No items selected." );
				return false;
			}
		}
		
		$cids = $k . '=' . implode ( ' OR ' . $k . '=', $cid );
		
		$query = 'UPDATE ' . $this->_tbl . ' SET published = ' . ( int ) $publish . ' WHERE (' . $cids . ')';
		
		$checkin = in_array ( 'checked_out', array_keys ( $this->getProperties () ) );
		if ($checkin) {
			$query .= ' AND (checked_out = 0 OR checked_out = ' . ( int ) $user_id . ')';
		}
		
		$this->_db->setQuery ( $query );
		if (! $this->_db->query ()) {
			$this->setError ( $this->_db->getErrorMsg () );
			return false;
		}
		
		if (count ( $cid ) == 1 && $checkin) {
			if ($this->_db->getAffectedRows () == 1) {
				$this->checkin ( $cid [0] );
				if ($this->$k == $cid [0]) {
					$this->published = $publish;
				}
			}
		}
		$this->setError ( '' );
		return true;
	}
	
	/**
	 * Export item list to xml
	 *
	 * @access public
	 * @param boolean Map foreign keys to text values
	 */
	function toXML($mapKeysToText = false) {
		$xml = '<record table="' . $this->_tbl . '"';
		
		if ($mapKeysToText) {
			$xml .= ' mapkeystotext="true"';
		}
		$xml .= '>';
		foreach ( get_object_vars ( $this ) as $k => $v ) {
			if (is_array ( $v ) or is_object ( $v ) or $v === NULL) {
				continue;
			}
			if ($k [0] == '_') { // internal field
				continue;
			}
			$xml .= '<' . $k . '><![CDATA[' . $v . ']]></' . $k . '>';
		}
		$xml .= '</record>';
		
		return $xml;
	}
	
	/**
	 * Add a directory where JTable should search for table types. You may
	 * either pass a string or an array of directories.
	 *
	 * @access	public
	 * @param	string	A path to search.
	 * @return	array	An array with directory elements
	 * @since 1.5
	 */
	function addIncludePath($path = null) {
		static $paths;
		
		if (! isset ( $paths )) {
			$paths = array (dirname ( __FILE__ ) . DS . 'table' );
		}
		
		// just force path to array
		settype ( $path, 'array' );
		
		if (! empty ( $path ) && ! in_array ( $path, $paths )) {
			// loop through the path directories
			foreach ( $path as $dir ) {
				// no surrounding spaces allowed!
				$dir = trim ( $dir );
				
				// add to the top of the search dirs
				// so that custom paths are searched before core paths
				array_unshift ( $paths, $dir );
			}
		}
		return $paths;
	}
}
